/**
 * GWT Kad : Kademlia implementation for GWT
 * Copyright (C) 2012  Pierrick HYMBERT, Paul-André MISTRAL, Florian ALEXANDRE
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package org.kad.gwt.web.client.applet.internal.services;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.kad.gwt.web.client.applet.GWTKadApplet;
import org.kad.gwt.web.client.applet.AppletListener;
import org.kad.gwt.web.client.applet.EventType;
import org.kad.gwt.web.client.applet.ServerResponseCallback;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

/**
 * Applet final service.
 * 
 * @author Pierrick HYMBERT &lt;pierrick [dot] hymbert [at] gmail [dot] com&gt;
 */
public class GWTKadAppletService implements GWTKadApplet {
    /**
     * serial uid.
     */
    private static final long                    serialVersionUID     = -2758676662937090794L;
    /**
     * The default buffer size to use.
     */
    private static final int                     DEFAULT_BUFFER_SIZE  = 1024 * 4;                                      // 4ko
    /**
     * The max size accepted for a message.
     */
    private static final int                     MAX_MESSAGE_SIZE     = 1024 * 10;                                     // 10ko
    /**
     * The listeners.
     */
    private Map<EventType, List<AppletListener>> listenersByEventType = new HashMap<EventType, List<AppletListener>>();
    /**
     * The http server.
     */
    private HttpServer                           server;
    /**
     * The applet logger.
     */
    private Logger                               logger;

    /**
     * Constructor.
     * 
     * @param logger
     *            The logger
     */
    public GWTKadAppletService(final Logger loggerIn) {
        this.logger = loggerIn;
    }

    /**
     * {@inheritDoc}
     */
    public void startServer(final int port, final String uri) {
        try {
            server = HttpServer.create(new InetSocketAddress(port), 0);
            server.createContext(uri, new HttpHandler() {
                @Override
                public void handle(final HttpExchange httpExchange) throws IOException {
                    handleHttpRequest(httpExchange);
                }

            });
            server.setExecutor(null);
            server.start();
            logger.info("Server started on " + server.getAddress());
        } catch (Exception e) {
            stopServer();
            handleException(e);
        }
    }

    /**
     * Handle the response.
     * 
     * @param httpExchange
     *            The http exchange
     * @param responseCode
     *            The response code
     * @param response
     *            The response string
     * @throws IOException
     *             If an error occurs
     */
    void sendResponse(final HttpExchange httpExchange, int responseCode, String response) {
        sendResponse(httpExchange, responseCode, response, createDefautHeaders(httpExchange));
    }

    /**
     * Handle the response.
     * 
     * @param httpExchange
     *            The http exchange
     * @param responseCode
     *            The response code
     * @param response
     *            The response string
     * @param headers
     *            The response headers
     * @throws IOException
     *             If an error occurs
     */
    void sendResponse(final HttpExchange httpExchange, int responseCode, String response, Map<String, String> headers) {
        try {
            logger.debug("RESP " + responseCode + " " + response);
            byte[] responseBytes = response.getBytes();
            headers.put("Content-Length", String.valueOf(responseBytes.length));
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpExchange.getResponseHeaders().add(header.getKey(), header.getValue());
            }
            httpExchange.sendResponseHeaders(responseCode, responseBytes.length);
            OutputStream os = httpExchange.getResponseBody();
            try {
                os.write(responseBytes);
            } finally {
                os.close();
            }
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Handle the http request
     * 
     * @param httpExchange
     *            The http exchange
     */
    void handleHttpRequest(final HttpExchange httpExchange) {
        try {
            String requestMethod = httpExchange.getRequestMethod();
            String uri = httpExchange.getRequestURI().toString();
            StringWriter sw = new StringWriter();
            InputStream input = httpExchange.getRequestBody();
            InputStreamReader in = new InputStreamReader(input);
            char[] buffer = new char[DEFAULT_BUFFER_SIZE];
            long count = 0;
            int n = 0;
            logger.debug("Method=" + httpExchange.getRequestMethod());
            for (Map.Entry<String, List<String>> header : httpExchange.getRequestHeaders().entrySet()) {
                logger.debug(header.getKey() + "=" + header.getValue());
            }
            logger.debug("n=" + n);
            while (-1 != (n = in.read(buffer))) {
                logger.debug("n=" + n + ", message=" + sw.toString());
                sw.write(buffer, 0, n);
                count += n;
                if (count > MAX_MESSAGE_SIZE) {
                    logger.error("A too big message was sent to the server ...");
                    handleException(new Exception("A too big message was received from " + httpExchange.getRemoteAddress()));
                    sendResponse(httpExchange, 403, "Access denied");
                    return;
                }
            }
            String message = sw.toString();
            logger.debug("uri=" + uri + ", message=" + message);
            if ("OPTIONS".equals(requestMethod)) {
                handleCrossDomainRequest(httpExchange);
            } else {
                fireEvent(EventType.MESSAGE, uri, message, new ServerResponseCallback() {
                    @Override
                    public void answer(int responseCode, String response) {
                        try {
                            sendResponse(httpExchange, responseCode, response);
                        } catch (Exception e) {
                            sendResponse(httpExchange, 500, "Internal Server Error (GWTKad still in progress :))");
                            handleException(e);
                        }
                    }

                });
            }
        } catch (Exception e) {
            sendResponse(httpExchange, 500, "Internal Server Error (GWTKad still in progress :))");
            handleException(e);
        }
    }

    /**
     * @param httpExchange
     */
    private void handleCrossDomainRequest(HttpExchange httpExchange) {
        Map<String, String> headers = createDefautHeaders(httpExchange);
        sendResponse(httpExchange, 200, "", headers);
    }

    /**
     * @param httpExchange
     * @return
     */
    private Map<String, String> createDefautHeaders(HttpExchange httpExchange) {
        Map<String, String> headers = new HashMap<String, String>();
        String origin = httpExchange.getRequestHeaders().getFirst("Origin");
        if (origin == null || origin.length() == 0) {
            origin = "*";
        }
        String accessControlHeaders = httpExchange.getRequestHeaders().getFirst("Access-Control-Request-Headers");
        if (accessControlHeaders == null || accessControlHeaders.length() == 0) {
            accessControlHeaders = "*";
        }
        headers.put("Access-Control-Allow-Origin", origin);
        headers.put("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
        headers.put("Access-Control-Max-Age", "1728000");
        headers.put("Access-Control-Allow-Headers", accessControlHeaders);
        headers.put("Keep-Alive", "timeout=2, max=100");
        headers.put("Connection", "Keep-Alive");
        headers.put("Date", new Date().toGMTString());
        headers.put("Server", "Apache 2.2");
        headers.put("Content-Type", "text/plain");
        return headers;
    }

    /**
     * Handle an exceptio nand fire to listeners.
     * 
     * @param e
     *            The exception
     */
    void handleException(final Exception e) {
        StringWriter stringWriter = new StringWriter();
        PrintWriter pw = new PrintWriter(stringWriter);
        e.printStackTrace(pw);
        pw.close();
        String errorMessage = e.getMessage() + "\n" + stringWriter.toString();
        logger.error(errorMessage);
        fireEvent(EventType.ERROR, errorMessage, null, null);
    }

    /**
     * Fire an event to the listeners of type <code>event</code>
     * 
     * @param event
     *            The event type
     * @param param1
     *            1st parameter (error message or request uri)
     * @param param2
     *            2nd parameter (request body)
     * @param param3
     *            3rd parameter (response call back)
     */
    void fireEvent(EventType event, Object param, Object param2, Object param3) {
        List<AppletListener> listeners = listenersByEventType.get(event);
        if (listeners != null) {
            for (AppletListener listener : listeners) {
                try {
                    listener.handle(param, param2, param3);
                } catch (Exception e) {
                    handleException(e);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void stopServer() {
        if (server != null) {
            server.stop(0);
        }
        server = null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addErrorListener(AppletListener errorListener) {
        addListener(EventType.ERROR, errorListener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addMessageListener(AppletListener messageListener) {
        addListener(EventType.MESSAGE, messageListener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeErrorListener(AppletListener errorListener) {
        removeListener(EventType.ERROR, errorListener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getListenPort() {
        return server.getAddress().getPort();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isReady() {
        return server != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeMessageListener(AppletListener messageListener) {
        removeListener(EventType.MESSAGE, messageListener);
    }

    /**
     * Add a listener of the specified type.
     * 
     * @param eventType
     *            The event
     * @param listener
     *            The listener
     */
    private void addListener(EventType eventType, AppletListener listener) {
        List<AppletListener> listeners = listenersByEventType.get(eventType);
        if (listeners == null) {
            listeners = new ArrayList<AppletListener>();
            listenersByEventType.put(eventType, listeners);
        }
        listeners.add(listener);
    }

    /**
     * Remove a listener of this type.
     * 
     * @param eventType
     *            The event type
     * @param listener
     *            The listener to remove
     */
    private void removeListener(EventType eventType, AppletListener listener) {
        List<AppletListener> listeners = listenersByEventType.get(eventType);
        if (listeners != null) {
            listeners.remove(listener);
        }
    }
}
